#region File Description
//-----------------------------------------------------------------------------
// ParticleSystem.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion

#region Using Statements
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using XDL.Framework.Collections;
using Microsoft.Xna.Framework.Graphics.PackedVector;
#endregion

namespace XDL.Framework.Particles
{
    /// <summary>
    /// The main component in charge of displaying particles.
    /// </summary>
    public class ParticleSystem : 
        DrawableGameComponent,
        UpdatableArray<ParticleVertex>.Client
    {
        #region Fields


        // Name of the XML settings file describing this particle system.
        string m_settingsName;


        // Settings class controls the appearance and animation of this particle system.
        ParticleSettings m_settings;

        /// <summary>
        /// Gets the appearance and animation of this particle system.
        /// </summary>
        public ParticleSettings Settings
        {
            get
            {
                return m_settings;
            }
        }

        /// <summary>
        /// External handler to update particle position (ex: collision management)
        /// </summary>
        /// <param name="gameTime">Upate time.</param>
        /// <param name="particle">Particle being updated.</param>
        /// <returns>false if the particle is to killed, true otherwise.</returns>
        public delegate bool ParticleUpdateHandler(
            GameTime gameTime,
            ParticleVertex oldParticle,
            ref ParticleVertex particle);

        /// <summary>
        /// External particle handler (optional)
        /// </summary>
        public ParticleUpdateHandler OnParticleUpdate = null;

        // For loading the effect and particle texture.
        ContentManager m_content;

        // Custom effect for drawing point sprite particles. This computes the particle
        // animation entirely in the vertex shader: no per-particle CPU work required!
        Effect m_particleEffect;

        // Shortcuts for accessing frequently changed effect parameters.
        EffectParameter m_effectViewParameter;
        EffectParameter m_effectProjectionParameter;
        EffectParameter m_effectViewportScaleParameter;

        // An array of particles, treated as a circular queue.
        UpdatableArray<ParticleVertex> m_particles;

        // A vertex buffer holding our particles. This contains the same data as
        // the particles array, but copied across to where the GPU can access it.
        DynamicVertexBuffer m_vertexBuffer;

        // Index buffer turns sets of four vertices into particle quads (pairs of triangles).
        IndexBuffer m_indexBuffer;

        // Shared random number generator.
        static Random random = new Random();
        #endregion

        #region Initialization


        /// <summary>
        /// Constructor.
        /// </summary>
        public ParticleSystem(Game game, ContentManager content, string settingsName)
            : base(game)
        {
            this.m_content = content;
            this.m_settingsName = settingsName;
        }
        
        /// <summary>
        /// Loads graphics for the particle system.
        /// </summary>
        protected override void LoadContent()
        {
            m_settings = m_content.Load<ParticleSettings>(m_settingsName);

            m_particles = new UpdatableArray<ParticleVertex>(m_settings.MaxParticles * 4, 4, this);

            for (int i = 0; i < m_settings.MaxParticles; i++)
            {
                m_particles.Items[i * 4 + 0].Corner = new Short2(-1, -1);
                m_particles.Items[i * 4 + 1].Corner = new Short2(1, -1);
                m_particles.Items[i * 4 + 2].Corner = new Short2(1, 1);
                m_particles.Items[i * 4 + 3].Corner = new Short2(-1, 1);
            }

            InitializeParticleEffect();

            // Create a dynamic vertex buffer.
            m_vertexBuffer = new DynamicVertexBuffer(GraphicsDevice, typeof(ParticleVertex),
                                                   m_settings.MaxParticles * 4, BufferUsage.WriteOnly);

            // Create and populate the index buffer.
            ushort[] indices = new ushort[m_settings.MaxParticles * 6];

            for (int i = 0; i < m_settings.MaxParticles; i++)
            {
                indices[i * 6 + 0] = (ushort)(i * 4 + 0);
                indices[i * 6 + 1] = (ushort)(i * 4 + 1);
                indices[i * 6 + 2] = (ushort)(i * 4 + 2);

                indices[i * 6 + 3] = (ushort)(i * 4 + 0);
                indices[i * 6 + 4] = (ushort)(i * 4 + 2);
                indices[i * 6 + 5] = (ushort)(i * 4 + 3);
            }

            m_indexBuffer = new IndexBuffer(GraphicsDevice, typeof(ushort), indices.Length, BufferUsage.WriteOnly);

            m_indexBuffer.SetData(indices);          
        }


        /// <summary>
        /// Helper for initializing the particle effect.
        /// </summary>
        void InitializeParticleEffect()
        {
            m_particleEffect = m_settings.ParticleEffect;

            EffectParameterCollection parameters = m_particleEffect.Parameters;

            // Look up shortcuts for parameters that change every frame.
            m_effectViewParameter = parameters["View"];
            m_effectProjectionParameter = parameters["Projection"];
            m_effectViewportScaleParameter = parameters["ViewportScale"];

            // Choose the appropriate effect technique.
            m_particleEffect.CurrentTechnique = m_particleEffect.Techniques[m_settings.TechniqueName];
        }


        #endregion

        #region Update and Draw

        /// <summary>
        /// Updates the particle system.
        /// </summary>
        public override void Update(GameTime gameTime)
        {
            m_particles.Update(gameTime);
        }
     

        /// <summary>
        /// Draws the particle system.
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            // nothing to do?
            if (m_particles.ActiveItems == 0)
                return;

            GraphicsDevice gd = GraphicsDevice;

            // set render states
            gd.BlendState =  m_settings.BlendState;
            gd.DepthStencilState = DepthStencilState.DepthRead;

            // Set the particle vertex buffer and vertex declaration.
            m_vertexBuffer.SetData<ParticleVertex>(m_particles.Items);
            gd.SetVertexBuffer(m_vertexBuffer);
            gd.Indices = m_indexBuffer;

            foreach (EffectPass pass in m_particleEffect.CurrentTechnique.Passes)
            {
                pass.Apply();

                gd.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0,
                    0, m_particles.ActiveItems * 4,
                    0, m_particles.ActiveItems * 2);
            }

            // Reset a couple of the more unusual renderstates that we changed,
            // so as not to mess up any other subsequent drawing.
            gd.DepthStencilState = DepthStencilState.Default;
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Clears the particle system.
        /// </summary>
        public void Clear()
        {
            m_particles.Clear();
        }


        /// <summary>
        /// Sets the camera view and projection matrices
        /// that will be used to draw this particle system.
        /// </summary>
        public void SetCamera(Matrix view, Matrix projection)
        {
            m_effectViewParameter.SetValue(view);
            m_effectProjectionParameter.SetValue(projection);
        }

        public void SetViewport(Viewport vport)
        {
            // Set an effect parameter describing the viewport size. This is
            // needed to convert particle sizes into screen space point sizes.
            m_effectViewportScaleParameter.SetValue(new Vector2(0.5f / vport.AspectRatio, -0.5f));            
        }

        /// <summary>
        /// Adds a new particle to the system.
        /// </summary>
        /// <param name="position">Particle's position.</param>
        /// <param name="velocity">Initial velocity.</param>
        public void AddParticle(Vector3 position, Vector3 velocity)
        {
            // adjust the lifetime with the randomness factor
            AddParticle(0, position, velocity);
        }

        /// <summary>
        /// Adds a particle to the system with a defined age.
        /// </summary>
        /// <param name="position">Particle's position.</param>
        /// <param name="velocity">Initial velocity.</param>
        /// <param name="normalizedAge">Age in a young(0) to old(1) scale</param>
        public void AddParticle(Vector3 position, Vector3 velocity, float normalizedAge)
        {
            // adjust the lifetime with the randomness factor
            AddParticle(0, position, velocity, (float)random.NextDouble() * m_settings.DurationRandomness);
        }


                /// <summary>
        /// Adds a new particle to the system.
        /// </summary>
        /// <param name="position">Particle's position.</param>
        /// <param name="velocity">Initial velocity.</param>
        public void AddParticle(float tag, Vector3 position, Vector3 velocity)
        {
            // adjust the lifetime with the randomness factor
            AddParticle(tag, position, velocity, (float)random.NextDouble() * m_settings.DurationRandomness);
        }

        /// <summary>
        /// Adds a particle to the system with a defined age.
        /// </summary>
        /// <param name="position">Particle's position.</param>
        /// <param name="velocity">Initial velocity.</param>
        /// <param name="normalizedAge">Age in a young(0) to old(1) scale</param>
        public void AddParticle(float tag, Vector3 position, Vector3 velocity, float normalizedAge)
        {
            // Adjust the input velocity based on how much
            // this particle system wants to be affected by it.
            velocity *= m_settings.EmitterVelocitySensitivity;

            // Add in some random amount of horizontal velocity.
            /*
            float horizontalVelocity = MathHelper.Lerp(m_settings.MinHorizontalVelocity,
                                                       m_settings.MaxHorizontalVelocity,
                                                       (float)random.NextDouble());
            double horizontalAngle = random.NextDouble() * MathHelper.TwoPi;
            velocity.Z += horizontalVelocity * (float)Math.Sin(horizontalAngle);
            */

            velocity.X += MathHelper.Lerp(m_settings.MinHorizontalVelocity,
                                          m_settings.MaxHorizontalVelocity,
                                          (float)random.NextDouble());

            // Add in some random amount of vertical velocity.
            velocity.Y += MathHelper.Lerp(m_settings.MinVerticalVelocity,
                                          m_settings.MaxVerticalVelocity,
                                          (float)random.NextDouble());
            // Choose four random control values. These will be used by the vertex
            // shader to give each particle a different size, rotation, and color.
            Color randomValues = new Color((byte)random.Next(255),
                                           (byte)random.Next(255),
                                           (byte)random.Next(255),
                                           (byte)random.Next(255));

            // Fill in the particle vertex structure.
            ParticleVertex newParticle = new ParticleVertex();
            newParticle.Tag = tag;
            newParticle.Position = position;
            newParticle.Velocity = velocity;
            newParticle.Random = randomValues;
            // set the age
            newParticle.Lifetime = (float)m_settings.Duration.TotalSeconds * (1.0f - normalizedAge);
            // add the 4 corners
            newParticle.Corner = new Short2(-1, -1);
            m_particles.Add(newParticle);
            newParticle.Corner = new Short2(1, -1);
            m_particles.Add(newParticle);
            newParticle.Corner = new Short2(1, 1);
            m_particles.Add(newParticle);
            newParticle.Corner = new Short2(-1, 1);
            m_particles.Add(newParticle);
        }
        #endregion

        #region Client Members

        public virtual void Copy(ref ParticleVertex item, ref ParticleVertex targetItem)
        {
            // backup constant            
            Short2 corner = targetItem.Corner;
            float tag = targetItem.Tag;
            // copy
            targetItem = item;
            // restore corner information
            targetItem.Corner = corner;
            // restore custom information
            targetItem.Tag = tag;
        }

        /// <summary>
        /// Update the particle state (CPU)
        /// </summary>
        /// <param name="dt">Delta time</param>
        /// <param name="item">Partile to update</param>
        /// <returns>true if the particle is still active.</returns>
        public virtual bool Update(GameTime gameTime, ref ParticleVertex item)
        {
            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
            item.Lifetime -= dt;
            if (item.Lifetime > 0)
            {
                // get previous state
                ParticleVertex currentParticle = item;
                // as per Shawn Heargraves recommendations, don't use overloaded operators
                item.Position.X += dt * item.Velocity.X;
                item.Position.Y += dt * item.Velocity.Y;
                item.Position.Z += dt * item.Velocity.Z;

                // basic rebound behavior
                if (m_settings.GroundLevel.HasValue && item.Position.Y < m_settings.GroundLevel.Value)
                {
                    item.Position.Y = m_settings.GroundLevel.Value;
                    item.Velocity.Y = -item.Velocity.Y;
                }
                item.Velocity.X += dt * m_settings.Gravity.X;
                item.Velocity.Y += dt * m_settings.Gravity.Y;
                item.Velocity.Z += dt * m_settings.Gravity.Z;
                // damping
                item.Velocity.X -= dt * (1-m_settings.EndVelocity) * item.Velocity.X;
                item.Velocity.Y -= dt * (1-m_settings.EndVelocity) * item.Velocity.Y;
                item.Velocity.Z -= dt * (1-m_settings.EndVelocity) * item.Velocity.Z;

                // delegate to client (intersection check for ex.)
                bool isActive = true;
                if (OnParticleUpdate != null)
                    isActive = OnParticleUpdate(gameTime,currentParticle, ref item);
              
                return isActive;
            }

            return false;
        }

        #endregion
    }
}
